package redislock;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @Description 扣库存案例模拟分布式锁问题
 * @Date 2020/7/14 10:28
 * @Author taojiangbing
 */
public class TestLock {

    private static RedisTemplate redisTemplate;

    private static String key = "lock";

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
        redisTemplate = context.getBean(RedisTemplate.class);

        for (int i = 0; i < 15; i++) {
            new Thread(() -> {
                deductStock();
            }, "Thread-" + i).start();
        }
    }

    /**
     * 1.单线程环境下，不会出现这种库存安全问题
     * 2.多线程下，在
     * int realStock = stock - 1;
     * redisTemplate.opsForValue().set("stock", realStock + "");
     * 这两行有线程安全问题，多个线程同时进来，第一个线程还没扣减库存，第二个线程进来获取的库存是10 ，第一个线程扣减库存，第二个线程
     * 拿10 -1 的数据再次去扣减库存，覆盖了第一次扣减库存的数据，这时等于第一个线程库存压根没扣减
     * 解决方式：锁
     * 因为是在分布式环境，所以要分布式锁，利用的特性就是redis单线程特性
     * 存在的问题
     * 1.死锁问题 加锁之后，如果没有及时释放，或者在释放锁的一刹那redis宕机了，都有可能导致
     * 解决办法：
     * 1）业务代码放到try catch代码块中，释放锁放到finally中保证发生代码异常时锁的释放
     * 2）给锁加上超时时间，防止在删除锁时，redis发生宕机，锁永远得不到释放，加上了超时时间后，锁到了时间会释放
     * <p>
     * 2.锁失效问题
     * 场景1.：再上一步中设置了锁超时时间，那么如果业务操作时间 > timeout 也就是说业务还没有释放锁，锁就过期了，
     * 这个时候其他线程就可以获得锁了,存在线程安全问题
     * 场景2：在超高并发场景下，我们设置key的过期时间是10秒，如果第一个线程过来会执行15秒的话，在高并发的情况下，
     * 很有可能会出现第一个线程释放掉了第二个线程的锁，第二个线程又释放了第三个线程的锁，而导致锁永久失效。
     * 解决办法：
     * 1）定时任务去监控timeout,如果发现剩余的时间 < 10/3 ,给redis键续命
     * 2）给定uuid，线程自己只能释放自己的锁
     * 3.锁可重入问题
     * 解决办法：
     * 1）采用线程id记录，同一个线程id获取了锁，则进去其他锁代码块，可自由通行
     */
    private static void deductStock() {
        //需要保证扣减库存是个原子性的操作，不允许其他线程加塞
        String uuid = UUID.randomUUID().toString();
        boolean lock = redisTemplate.opsForValue().setIfAbsent(key, uuid);
        redisTemplate.expire(key, 10, TimeUnit.SECONDS);
        try {
            while (!lock) {
                //空循环，自旋 这里存在性能问题，如果一直拿不到锁会消耗cpu性能
                Boolean b = redisTemplate.opsForValue().setIfAbsent(key, uuid);
                if (b) {
                    break;
                }
            }
            //获取现有库存
            int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock").toString());
            if (stock > 0) {
                //扣减现有库存
                int realStock = stock - 1;
                //重新存入redis
                redisTemplate.opsForValue().set("stock", realStock + "");
            } else {
                //现有库存不足
                System.out.println(Thread.currentThread().getName() + " 扣减失败，库存不足");
            }
        } catch (NumberFormatException e) {
            e.printStackTrace();
        } finally {
            if (redisTemplate.opsForValue().get(key).toString().equals(uuid)) {
                redisTemplate.delete(key);
            }
        }
    }
}
